package golf.simulation;

import golf.simulation.dataobject.InvestmentItem;
import golf.simulation.dataobject.SimulationOutputData;
import golf.simulation.dataobject.Transaction;
import golf.simulation.dataobject.TransactionType;
import golf.stockdata.framework.hbmdao.PriceHist;
import golf.stockdata.framework.util.hbm.HibernateUtil;

import java.math.BigDecimal;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Random;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.criterion.Property;

public class SimulationFormulaA {

	private static Logger logger = Logger.getLogger(SimulationFormulaA.class);
	private static Logger outputLogger = Logger.getLogger("SIMULATION_OUTPUT");
//	private static Logger outputLogger2 = Logger.getLogger("SIMULATION_OUTPUT_2");
	private static Random RANDOM_GENERATOR = new Random();
	
	/**
	 * Big Swing is when the price span across the sell-limit and the cut-limit
	 * in the same day
	 */
	private static int BIG_SWING_COUNTER = 0;
	public static double LOWEST_RUNNING_BALANCE=Double.MAX_VALUE;
	
	public static SimulationOutputData simulation_formula_A_MultiStream(
				double initialCapital,
				double fixedPricePerInvestment,
				double minimumBalancePercentage, //Guarantee that the whole capital should be kept at this level
				double absoluteMinimumBalance,
				int maxInvestmentItem, // maximum allowed investment items
				double pillowPercentage,
				double sellPercentage,
				double tfexGapVariation,
				double brokerFixedCost,
				double pricePerPoint,
				int numberOfAllowedLost,
				int inTheLast,
				int randomWaitLimit,
				Date startDate,
				Date endDate,
				String periodType,
				String marketName,
				String currencyPair,
				int smavInput){
	    	
	    	logger.debug(">>> simulation_formula_A_MultiStream");
	    	
	    	LOWEST_RUNNING_BALANCE=Double.MAX_VALUE;
	    	BIG_SWING_COUNTER = 0;
	    	
	    	if(maxInvestmentItem <= 0){
	    		maxInvestmentItem = 1;
	    	}
	    	
	    	double runningBalance = initialCapital;
	    	
	    	ArrayList<InvestmentItem> invstmntStreams = new ArrayList<InvestmentItem>();
	    	
	        Session session = HibernateUtil.createHibernatesession();
	        session.getTransaction().begin();
	        
	        @SuppressWarnings("unchecked")
			ArrayList<PriceHist> priceHistList = (ArrayList<PriceHist>)session.createCriteria(PriceHist.class, "main")
				.add(Property.forName("id.tradingDate").between(startDate, endDate) )
				.add(Property.forName("id.marketName").eq(marketName))
				.add(Property.forName("id.currencyPair").eq(currencyPair))
				.add(Property.forName("id.tradingType").eq(periodType)).list();
	        
	        int i=0;
			int previousMonth = 0;
			double lastMinimumAmount = 0;
			boolean hasDroppedAContractThisMonth = false;
			int contractAfterDropPeriod = 0;
			
	        for(PriceHist priceHist:priceHistList){
	        	i++;
	        	if(i > 15){
	        		
	            	double smav = getSMAV(smavInput, priceHist.getId().getTradingDate(), priceHistList);
	            	
	        		int currentMonth = getMonth(priceHist.getId().getTradingDate());
	        		
	        		if(contractAfterDropPeriod > 0){
	        			contractAfterDropPeriod--;
	        		}
	        		
	        		if(currentMonth != previousMonth
	        				&& contractAfterDropPeriod == 0){
	        			
	        			hasDroppedAContractThisMonth = false;
	        			
	        			logger.debug("Month changed");
	        			logger.debug("remaining balance:" + runningBalance);
	        			//Money handling and check if we can get a new investment stream
	        			if(runningBalance > 0
	        					&& (runningBalance + fixedPricePerInvestment)/runningBalance > minimumBalancePercentage
	        					&& invstmntStreams.size() < maxInvestmentItem
	        					&& runningBalance > absoluteMinimumBalance){
	        				
	        				//Keep lastMinimumAmount for later use
	        				lastMinimumAmount = runningBalance * minimumBalancePercentage;
	        				//Safe guarantee remaining, add one more investment stream
	        				invstmntStreams.add(new InvestmentItem());
	        				logger.debug("Added a new contract:" + invstmntStreams.get(invstmntStreams.size() - 1).getUUID());
	        				runningBalance = runningBalance + fixedPricePerInvestment;
	        				outputLogger.info("Cost of adding new contract:" + invstmntStreams.get(invstmntStreams.size() - 1).getUUID() + ";;;;;;;;;" + fixedPricePerInvestment + ";" + BigDecimal.valueOf(runningBalance).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	        			}
	        		}
	        		
	        		//Find the poorest performing, in case we need to drop one.
	        		InvestmentItem thePoorest = null;
	        		if(invstmntStreams.size() > 1){
	            		thePoorest = findThePoorestPerformingContract(invstmntStreams, priceHist.getId().getTradingDate());
	        		}
	        		
	        		for(InvestmentItem invstmnt : invstmntStreams){
	        			
	        			if(invstmnt.getForceIdle() == 0
	        					&& !invstmnt.isDiscontinued()){
	        				//started investment processing
	        				
	            			logger.debug("Processing:" + invstmnt.getUUID());
	            			
	                    	if(invstmnt.isFirstTime()){
	                    		
	                    		logger.debug("First time block");
	                    		
	                    		invstmnt.setCurrentTransaction(firstTimeProcess(smav, priceHist, pillowPercentage, sellPercentage));
	                    		
	                    		if(invstmnt.getCurrentTransaction() != null){
	                    			invstmnt.setFirstTime(false);
	                    		}
	                    	}else{
	                    		
	                    		logger.debug("Not the first time block");
	                    		
	                    		if(invstmnt.getCurrentTransaction() != null){
	                    			
	                        		if(invstmnt.getCurrentTransaction().pillowIsThere()){
	                        			logger.debug("Pillow is still there, check if needs removed");
	                            		pillowSet(priceHist, invstmnt.getCurrentTransaction(), tfexGapVariation);
	                        		}
	                        		
	                        		if(!isToHold(priceHist, invstmnt.getCurrentTransaction(), tfexGapVariation, brokerFixedCost, pricePerPoint)){
	                        			
	                        			logger.debug("Not to hold this item, time to take profit or loss");
	                        			
	                        			runningBalance = runningBalance + invstmnt.getCurrentTransaction().getProfitLoss();
	                        			
	                        			invstmnt.getCurrentTransaction().setSellDate(priceHist.getId().getTradingDate());
	                        			
	                        			outputLogger.info(invstmnt.getUUID() + ";" + invstmnt.getCurrentTransaction().toString() + ";" + BigDecimal.valueOf(runningBalance).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";" + BigDecimal.valueOf(lastMinimumAmount).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                        			
	                        			invstmnt.addTransaction(invstmnt.getCurrentTransaction());
	                        			invstmnt.setCurrentTransaction(null);
	                        			
	                        			if(thePoorest != null
	                        					&& ((runningBalance < lastMinimumAmount && lastMinimumAmount > absoluteMinimumBalance)
	                        					|| (lastMinimumAmount < absoluteMinimumBalance && runningBalance < absoluteMinimumBalance))
	                        					&& ! hasDroppedAContractThisMonth
	                        					&& invstmnt.getUUID().equals(thePoorest.getUUID())
	                        					&& invstmntStreams.size() > 1){
	                        				//Discontinue the poorest
	                        				invstmnt.discontinued();
	                        				hasDroppedAContractThisMonth = true;
	                        				runningBalance = runningBalance - fixedPricePerInvestment;
	                        				outputLogger.info(thePoorest.getUUID() + " is dropped;;;;;;;;;" + (0-fixedPricePerInvestment) + ";" + BigDecimal.valueOf(runningBalance).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                        				contractAfterDropPeriod = 30;
	                        			}else{
	                        				
	                        				logger.debug("Try to continue the investment");
	                        				
	                            			invstmnt.setCurrentTransaction(firstTimeProcess(smav, priceHist, pillowPercentage, sellPercentage));
	                                		if(invstmnt.getCurrentTransaction() != null){
	                                			invstmnt.setFirstTime(false);
	                                		}else{
	                                			invstmnt.setFirstTime(true);
	                                		}
	                            			
	                            			invstmnt.setForceIdle(setForcedIdle(invstmnt.getTransactionList(), numberOfAllowedLost, inTheLast, randomWaitLimit));
	                            			
	                        			}
	                        			
	                        		}
	                        		
	                    		}
	                    		
	                    	}
	        			}else{
	                		if(invstmnt.getForceIdle() > 0){
	                			invstmnt.deductForceIdle();
	                		}
	        			}
	        		}
	        		
	            	previousMonth = getMonth(priceHist.getId().getTradingDate());
	            	
	        	}
	        	
	        	if(runningBalance < LOWEST_RUNNING_BALANCE){
	        		LOWEST_RUNNING_BALANCE = runningBalance;
	        	}
	        }
	        
	        logger.info("Total Contracts:" + invstmntStreams.size());
	        int countDiscontinue = 0;
	        for(InvestmentItem item:invstmntStreams){
	        	if(item.isDiscontinued())
	        		countDiscontinue++;
	        }
	        logger.info("Discontinued Contracts:" + countDiscontinue);
	        logger.info("Total active Contracts:" + (invstmntStreams.size() - countDiscontinue) );
	        
	        outputLogger.info("Add back the fixed cost of all holding " + (invstmntStreams.size() - countDiscontinue) + " contracts;;;;;;;;;;" + BigDecimal.valueOf(runningBalance + ((invstmntStreams.size() - countDiscontinue) * (0-fixedPricePerInvestment))).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	        
	        
	//        for(Transaction trn:transList){
	//        	System.out.println(trn.toString());
	//        }
	        
	        session.getTransaction().rollback();
	        session.close();
	        
	        SimulationOutputData out = new SimulationOutputData();
	        out.setInvestmentStream(invstmntStreams);
	        out.setRunningBalance(runningBalance);
	        out.setContractHoldingBalance((invstmntStreams.size() - countDiscontinue) * (0-fixedPricePerInvestment));
	        out.setNumberOfAllActiveContract(invstmntStreams.size() - countDiscontinue);
	        out.setNumberOfAllContract(invstmntStreams.size());
	        out.setNumberOfAllDiscontinuedContract(countDiscontinue);
	        
	        logger.info("BIG Swing = " + BIG_SWING_COUNTER);
	        
	        logger.debug("<<< simulation_formula_A_MultiStream");
	        return out;
	        
	    }


	private static InvestmentItem findThePoorestPerformingContract(
			ArrayList<InvestmentItem> invstmntStreams, Date currentDate) {
		
		double minProfitMade = Double.MAX_VALUE;
		InvestmentItem theSelected = null;
		
		for(InvestmentItem item:invstmntStreams){
			
			if(!item.isDiscontinued()
					&& item.getTransactionList().size() > 5){
		    	double totalProfitLoss = 0;
	    		for(Transaction txn:item.getTransactionList()){
	    			totalProfitLoss = totalProfitLoss + txn.getProfitLoss();
	    		}
	    		if(totalProfitLoss < minProfitMade
	    				&& minProfitMade > 0){
	    			theSelected = item;
	    		}
			}
		}
		
		return theSelected;
	}


	private static int getMonth(Date dt){
		
		Calendar cal = Calendar.getInstance();
		cal.setTime(dt);
		return cal.get(Calendar.MONTH);
		
	}


	protected static Transaction firstTimeProcess(double smav, PriceHist priceHist, double pillowPercentage, double sellPercentage){
		
		logger.debug(">>> firstTimeProcess");
		Transaction txn = null;
		if(toLong(smav, priceHist)){
			txn = new Transaction(TransactionType.LONG, pillowPercentage, sellPercentage);
		}else if(toShort(smav, priceHist)){
			txn = new Transaction(TransactionType.SHORT, pillowPercentage, sellPercentage);
		}
		
		if(txn != null){
			txn.setBuyDate(priceHist.getId().getTradingDate());
			txn.setBuyPrice(priceHist.getClose().doubleValue());
			logger.debug("<<< firstTimeProcess(new investment transaction)");
			return txn;
		}else{
			logger.debug("<<< firstTimeProcess(null)");
			return null;
		}
		
	}


	private static int setForcedIdle(ArrayList<Transaction> trnList, int numberOfAllowedLost, int inTheLast, int randomWaitLimit){
		
		logger.debug(">>> setForcedIdle");
		
		int forcedIdle = 0;
		int countLoss = 0;
		
		double lastProfitLoss = trnList.get(trnList.size()-1).getProfitLoss();
		
		if(lastProfitLoss < 0){
			logger.debug("last time just loss");
			if(trnList.size() > inTheLast){
				
				for(int i=trnList.size()-1; i>trnList.size()-1-inTheLast; i--){
					if(trnList.get(i).getProfitLoss() < 0){
						countLoss++;
					}
				}
				
				logger.debug("Loss count=" + countLoss + " in the last " + inTheLast + " times");
				
				if(countLoss > numberOfAllowedLost){
					forcedIdle = RANDOM_GENERATOR.nextInt(randomWaitLimit);
				}
				
			}
		}else{
			logger.debug("Not loss recently, no check");
		}
		
		logger.debug("<<< setForcedIdle(" + forcedIdle + ")");
		
		return forcedIdle;
	}


	public static boolean isToHold(PriceHist priceHist, Transaction txn, double tfexGapVariation, double brokerFixedCost, double pricePerPoint){
		
		logger.debug(">>> isToHold");
		logger.debug("CurrentDate=" + priceHist.getId().getTradingDate().toString());
		logger.debug("TodayHigh=" + priceHist.getHigh());
		logger.debug("TodayLow=" + priceHist.getLow());
		logger.debug("Buy price=" + txn.getBuyPrice());
		logger.debug("Cut Limit=" + txn.getCutLimit());
		logger.debug("Sell Limit=" + txn.getSellLimit());
		logger.debug("Pillow Limit=" + txn.getPillowLimit());
		logger.debug("Tfex gap=" + tfexGapVariation);
		logger.debug("Pillow exist" + txn.pillowIsThere());
		logger.debug("Transaction type:" + txn.getTransactionType());
		
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			if(priceHist.getLow().doubleValue() < txn.getCutLimit()
					&& priceHist.getHigh().doubleValue() > txn.getSellLimit()){
				BIG_SWING_COUNTER++;
			}
		}else{
			if(priceHist.getHigh().doubleValue() > txn.getCutLimit()
					&& priceHist.getLow().doubleValue() < txn.getSellLimit()){
				BIG_SWING_COUNTER++;
			}
		}
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			logger.debug("LONG");
			if((priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getCutLimit())) > 0
					&& priceHist.getLow().subtract(BigDecimal.valueOf(txn.getCutLimit())).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)
					|| priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getCutLimit())) < 0){
				logger.debug("To cut");
				if(txn.pillowIsThere()){
					txn.setProfitLoss(brokerFixedCost);
				}else{
					double loss = (txn.getCutLimit() - txn.getBuyPrice()) * pricePerPoint + brokerFixedCost;
					txn.setProfitLoss(loss);
				}
				logger.debug("<<< isToHold(false)");
				return false;
				
			}else if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getSellLimit())) > 0
					|| (priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getSellLimit())) < 0
							&& BigDecimal.valueOf(txn.getSellLimit()).subtract(priceHist.getHigh()).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)){
				logger.debug("To take profit");
				if(priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getSellLimit())) > 0){
					//The low is higher than the sell limit
					txn.setProfitLoss(priceHist.getLow().subtract(BigDecimal.valueOf(txn.getPillowLimit())).doubleValue()*pricePerPoint + brokerFixedCost);
				}else{
					txn.setProfitLoss((txn.getSellLimit() - txn.getPillowLimit())*pricePerPoint + brokerFixedCost);
				}
				logger.debug("<<< isToHold(false)");
				return false;
			}
			
		}else if(txn.getTransactionType().equals(TransactionType.SHORT)){
			if((priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getSellLimit())) > 0
					&& priceHist.getLow().subtract(BigDecimal.valueOf(txn.getSellLimit())).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)
					|| priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getSellLimit())) < 0){
				logger.debug("To take profit");
				if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getSellLimit())) < 0){
					//The high is lower than the sell limit
					txn.setProfitLoss(BigDecimal.valueOf(txn.getPillowLimit()).subtract(priceHist.getHigh()).doubleValue()*pricePerPoint + brokerFixedCost);
				}else{
					txn.setProfitLoss((txn.getPillowLimit() - txn.getSellLimit())*pricePerPoint + brokerFixedCost);
				}
				return false;
			}else if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getCutLimit())) > 0
					|| (priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getCutLimit())) < 0
							&& BigDecimal.valueOf(txn.getCutLimit()).subtract(priceHist.getHigh()).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)){
				logger.debug("To cut");
				if(txn.pillowIsThere()){
					txn.setProfitLoss(brokerFixedCost);
				}else{
					double loss = (txn.getBuyPrice() - txn.getCutLimit()) * pricePerPoint + brokerFixedCost;
					txn.setProfitLoss(loss);
				}
				logger.debug("<<< isToHold(false)");
				return false;
				
			}
		}
		
		logger.debug("<<< isToHold(true)");
		return true;
	}


	public static void pillowSet(PriceHist priceHist, Transaction txn, double tfexGapVariation){
		
		logger.debug(">>> pillowSet");
		logger.debug("CurrentDate=" + priceHist.getId().getTradingDate().toString());
		logger.debug("TodayHigh=" + priceHist.getHigh());
		logger.debug("TodayLow=" + priceHist.getLow());
		logger.debug("Buy price=" + txn.getBuyPrice());
		logger.debug("Cut Limit=" + txn.getCutLimit());
		logger.debug("Sell Limit=" + txn.getSellLimit());
		logger.debug("Pillow Limit=" + txn.getPillowLimit());
		logger.debug("txn.getTransactionType()=" + txn.getTransactionType());
		logger.debug("tfexGapVariation=" + tfexGapVariation);
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) > 0
					|| (priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) < 0
							&& BigDecimal.valueOf(txn.getPillowLimit()).subtract(priceHist.getHigh()).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)){
				logger.debug("remove pillow");
				txn.removePillow();
			}
		}else if(txn.getTransactionType().equals(TransactionType.SHORT)){
			if((priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) > 0
					&& priceHist.getLow().subtract(BigDecimal.valueOf(txn.getPillowLimit())).compareTo(BigDecimal.valueOf(tfexGapVariation)) < 0)
					|| priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) < 0){
				logger.debug("remove pillow");
				txn.removePillow();
			}
		}
		
		logger.debug("<<< pillowSet");
		
	}


	private static double getSMAV(int smav, Date dateOfSMAV, ArrayList<PriceHist> priceHistList){
		
		int i=0;
		for(PriceHist priceHist:priceHistList){
			if(priceHist.getId().getTradingDate().equals(dateOfSMAV)
					|| priceHist.getId().getTradingDate().after(dateOfSMAV)){
				break;
			}
			i++;
		}
		
		if(i < smav -1){
			return -1;
		}
		
		BigDecimal out = BigDecimal.ZERO;
		for(int j=i;j>i-15;j--){
			out = out.add(priceHistList.get(j).getClose());
		}
		
		out = out.divide(BigDecimal.valueOf(smav), 2, BigDecimal.ROUND_HALF_UP);
		
		
		return out.doubleValue();
	}


	private static boolean toLong(double smav, PriceHist priceHist){
		if(priceHist.getLow().compareTo(BigDecimal.valueOf(smav)) > 0){
			return true;
		}else{
			return false;
		}
	}


	private static boolean toShort(double smav, PriceHist priceHist){
		if(priceHist.getHigh().compareTo(BigDecimal.valueOf(smav)) < 0){
			return true;
		}else{
			return false;
		}
	}
	

}
